/*
 * Copyright (C) 2006-2007 Eskil Bylund
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

using System;
using System.Text;
using System.Threading;
using System.Reflection;

using Gtk;
using Mono.Unix;

using DCSharp.Backend.Connections;
using DCSharp.Backend.Managers;
using DCSharp.Backend.Objects;
using DCSharp.Logging;
using DCSharp.Settings;
using DCSharp.Xml;

namespace DCSharp.Gui
{
	public class Gui
	{
		private static Thread mainThread;

		private Runtime runtime;
		private RuntimeSettings runtimeSettings;

		private InterfaceSettings settings;

		// Windows
		private MainWindow mainWindow;
		private HubWindow hubWindow;
		private PreferencesWindow preferencesWindow;

		// Pages
		private SearchPage searchPage;
		private DownloadPage downloadPage;
		private UploadPage uploadPage;
		private Page currentPage;

		static Gui()
		{
			mainThread = Thread.CurrentThread;

			// Initialize
			IconManager.Init();
#if GNOME
			Gnome.Vfs.Vfs.Initialize();
#endif
			AboutDialog.SetUrlHook(OpenUrl);
			Glade.XML.CustomHandler = CustomWidgetHandler;

			// Set up the style for the button
			Rc.ParseString(@"
			style 'dcsharp-small-button'
			{
				GtkWidget::focus-padding = 0
				xthickness = 0
				ythickness = 0
			}
			widget '*.dcsharp-close-button' style 'dcsharp-small-button'
			widget '*.dcsharp-criteria-button' style 'dcsharp-small-button'");

			// Events
			AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
		}

		public Gui(Runtime runtime, RuntimeSettings runtimeSettings,
			InterfaceSettings settings)
		{
			if (runtime == null)
			{
				throw new ArgumentNullException("runtime");
			}
			if (runtimeSettings == null)
			{
				throw new ArgumentNullException("runtimeSettings");
			}
			if (settings == null)
			{
				throw new ArgumentNullException("settings");
			}
			this.runtime = runtime;
			this.runtimeSettings = runtimeSettings;
			this.settings = settings;

			// Main Mindow
			mainWindow = new MainWindow(runtime.ConnectionManager,
				Globals.FavoriteManager);
			mainWindow.PageStyle = settings.PageStyle;

			RestoreWindowSettings();

			mainWindow.ToolbarVisible = settings.ToolbarVisible;
			mainWindow.KeyPressEvent += OnKeyPressEvent;
			mainWindow.Destroyed += delegate
			{
				SaveSettings();
				Application.Quit();
			};
			mainWindow.Show();

			// Events
			mainWindow.CurrentPageChanged += OnCurrentPageChanged;
			mainWindow.PageRemoved += OnPageRemoved;

			mainWindow.ConfigureEvent += OnWindowConfigureEvent;
			mainWindow.WindowStateEvent += OnWindowStateEvent;

			// Runtime Events
			runtime.HubManager.ConnectionCreated += OnHubConnectionCreated;
			runtime.HubManager.HubConnected += OnHubConnected;
			runtime.HubManager.HubDisconnected += OnHubDisconnected;
			runtime.DownloadManager.DownloaderAdded += OnDownloaderAdded;
			runtime.UploadManager.UploadStarted += OnUploadStarted;

			runtime.Error += OnRuntimeError;
		}

		#region Properties

		public MainWindow MainWindow
		{
			get { return mainWindow; }
		}

		#endregion

		#region Methods

		public static void InvokeAndWait(EventHandler handler)
		{
			InvokeAndWait(null, EventArgs.Empty, handler);
		}

		public static void InvokeAndWait(object sender, EventArgs args,
			EventHandler handler)
		{
			if (handler == null)
			{
				throw new ArgumentNullException("handler");
			}
			if (Thread.CurrentThread == mainThread)
			{
				handler(sender, args);
			}
			else
			{
				AutoResetEvent autoEvent = new AutoResetEvent(false);
				Application.Invoke(delegate
				{
					handler(sender, args);
					autoEvent.Set();
				});
				autoEvent.WaitOne();
			}
		}

		#region Pages

		public void ShowHub(HubConnection hub, bool present)
		{
			if (hub == null)
			{
				throw new ArgumentNullException("hub");
			}

			HubPage hubPage = mainWindow.Find<HubPage>(delegate(HubPage page)
			{
				return page.Hub.Equals(hub);
			});

			if (hubPage == null)
			{
				hubPage = new HubPage(hub);

				// Restore settings
				HubSettings hubSettings = settings.GetHubSettings(hub.Uri.ToString());
				if (hubSettings.VisibleColumns != 0)
				{
					hubPage.VisibleColumns = hubSettings.VisibleColumns;
				}
				hubPage.SizeAllocated += OnHubPageSizeAllocated;
			}
			else
			{
				hubPage.Hub = hub;
			}
			ShowPage(hubPage, present);
		}

		public void ShowSearch(bool present)
		{
			if (searchPage == null)
			{
				searchPage = new SearchPage(runtime.SearchManager);
				searchPage.SetSearchEnabled(runtime.HubManager.Count > 0);
				if (settings.SearchColumns != 0)
				{
					searchPage.VisibleColumns = settings.SearchColumns;
				}
			}
			ShowPage(searchPage, present);
			searchPage.FocusSearchEntry();
		}

		public void Search(SearchInfo searchInfo)
		{
			ShowSearch(true);
			searchPage.Search(searchInfo);
		}

		public void ShowDownloads(bool present)
		{
			if (downloadPage == null)
			{
				downloadPage = new DownloadPage(runtime.ConnectionManager,
					runtime.DownloadManager);

				if (settings.DownloadColumns != 0)
				{
					downloadPage.VisibleColumns = settings.DownloadColumns;
				}
				downloadPage.DirectoryTreeVisible = settings.DownloadPageDirectoryTreeVisible;
				downloadPage.SourcesVisible = settings.DownloadPageSourcesVisible;
				downloadPage.PanePosition = settings.DownloadPagePanePosition;
			}
			ShowPage(downloadPage, present);
		}

		public void ShowUploads(bool present)
		{
			if (uploadPage == null)
			{
				uploadPage = new UploadPage(runtime.UploadManager);
				if (settings.UploadColumns != 0)
				{
					uploadPage.VisibleColumns = settings.UploadColumns;
				}
			}
			ShowPage(uploadPage, present);
		}

		public ChatPage ShowChat(User localUser, User remoteUser, bool present)
		{
			if (localUser == null)
			{
				throw new ArgumentNullException("localUser");
			}
			if (remoteUser == null)
			{
				throw new ArgumentNullException("remoteUser");
			}

			ChatPage chatPage = mainWindow.Find<ChatPage>(
				delegate(ChatPage page)
			{
				return page.LocalUser == localUser &&
					page.RemoteUser == remoteUser;
			});

			if (chatPage == null)
			{
				chatPage = new ChatPage(localUser, remoteUser);
			}

			ShowPage(chatPage, present);
			return chatPage;
		}

		public FileListPage ShowFileList(FileList list, User user)
		{
			if (user == null)
			{
				throw new ArgumentNullException("user");
			}

			// Find or create the page
			FileListPage filePage = mainWindow.Find<FileListPage>(
				delegate(FileListPage page)
			{
				return page.User == user;
			});

			if (filePage == null)
			{
				filePage = new FileListPage(user, list);
			}
			else
			{
				filePage.FileList = list;
			}
			ShowPage(filePage, true);
			return filePage;
		}

		public void HandleListDownload(ListDownloader downloader,
			string pathToSelect)
		{
			if (downloader == null)
			{
				throw new ArgumentNullException("downloader");
			}
			// Find or create the page
			FileListPage filePage = ShowFileList(null, downloader.User);
			foreach (Widget child in filePage.Children)
			{
				child.Sensitive = false;
			}

			// Create the status box
			ListDownloadStatusArea statusArea = filePage.Children[0] as
				ListDownloadStatusArea;
			if (statusArea == null)
			{
				statusArea = new ListDownloadStatusArea(downloader);
				filePage.PackStart(statusArea, false, false, 0);
			}
			else
			{
				statusArea.Downloader = downloader;
			}
			statusArea.Sensitive = true;
			statusArea.Show();

			// Connect the two
			downloader.ListReceived += delegate
			{
				Application.Invoke(delegate
				{
					filePage.FileList = downloader.Result;
					if (pathToSelect != null)
					{
						filePage.SelectPath(pathToSelect);
					}
					foreach (Widget child in filePage.Children)
					{
						child.Sensitive = true;
					}
				});
				GLib.Timeout.Add(2000, delegate
				{
					if (statusArea.Downloader == downloader)
					{
						statusArea.Visible = false;
					}
					return false;
				});
			};
			ShowPage(filePage, true);
		}

		private void ShowPage(Page page, bool present)
		{
			if (!mainWindow.Contains(page))
			{
				mainWindow.Add(page);
			}
			if (present)
			{
				mainWindow.Present(page);
			}
		}

		private void OnHubPageSizeAllocated(object obj, EventArgs args)
		{
			HubPage hubPage = (HubPage)obj;
			HubSettings hubSettings = settings.GetHubSettings(hubPage.Hub.Uri.ToString());

			hubPage.PanePosition = hubPage.Allocation.Width - 
				hubSettings.SidebarWidth;

			hubPage.SizeAllocated -= OnHubPageSizeAllocated;
		}

		private void OnCurrentPageChanged(object obj, PageEventArgs args)
		{
			// Update the position of the new message separator.
			HubPage hubPage = currentPage as HubPage;
			if (hubPage != null)
			{
				hubPage.UpdateNewMessageSeparator();
				hubPage.ShowSeparatorOnMessage = true;
			}

			hubPage = args.Page as HubPage;
			if (hubPage != null)
			{
				hubPage.ShowSeparatorOnMessage = false;
			}
			currentPage = args.Page;
		}

		private void OnPageRemoved(object obj, PageEventArgs args)
		{
			Page page = args.Page;
			if (page != searchPage && page != downloadPage && page != uploadPage)
			{
				HubPage hubPage = page as HubPage;
				if (hubPage != null && hubPage.Hub != null)
				{
					// Save settings
					HubSettings hubSettings = settings.GetHubSettings(hubPage.Hub.Uri.ToString());
					hubSettings.SidebarWidth = hubPage.Allocation.Width - hubPage.PanePosition;
					hubSettings.VisibleColumns = hubPage.VisibleColumns;

					hubPage.Hub.Disconnect();
				}

				// Destroy the page in an idle handler so that any calls to
				// Application.Invoke have been handled.
				GLib.Idle.Add(delegate
				{
					page.Destroy();
					return false;
				});
			}
			else if (page == searchPage)
			{
				searchPage.StopSearch();
			}
		}

		#endregion

		#region Windows

		public void EditHubs()
		{
			if (hubWindow == null)
			{
				hubWindow = new HubWindow(runtime.HubManager,
					Globals.FavoriteManager, mainWindow);

				hubWindow.Window.SkipTaskbarHint = true;
				hubWindow.Window.Destroyed += delegate
				{
					hubWindow = null;
				};
			}
			hubWindow.Show();
		}

		public void EditPreferences()
		{
			if (preferencesWindow == null)
			{
				preferencesWindow = new PreferencesWindow(runtimeSettings,
					runtimeSettings.ConnectionSettings, settings,
					runtime.ShareManager, mainWindow);

				preferencesWindow.Window.Destroyed += delegate
				{
					try
					{
						Globals.SaveSettings();
					}
					catch
					{
					}
					preferencesWindow = null;
				};
			}
			preferencesWindow.Show();
		}

		public void ShowUserInformation(User user)
		{
			UserInformationDialog.Create(user, MainWindow).Show();
		}

		private void RestoreWindowSettings()
		{
			int x = settings.WindowX;
			int y = settings.WindowY;
			int width = settings.WindowWidth;
			int height = settings.WindowHeight;

			if (width != 0 && height != 0)
			{
				mainWindow.Resize(width, height);
			}
			if (x != 0 && y != 0)
			{
				mainWindow.Move(x, y);
			}

			if (settings.WindowMaximized)
			{
				mainWindow.Maximize();
			}
		}

		private void SaveWindowSettings()
		{
			int x, y, width, height;

			mainWindow.GetPosition(out x, out y);
			mainWindow.GetSize(out width, out height);

			settings.WindowX = x;
			settings.WindowY = y;
			settings.WindowWidth = width;
			settings.WindowHeight = height;
		}

		[GLib.ConnectBefore]
		private void OnWindowConfigureEvent(object obj, ConfigureEventArgs args)
		{
			Window window = (Window)obj;
			if ((window.GdkWindow.State & Gdk.WindowState.Maximized) != 0)
			{
				return;
			}
			SaveWindowSettings();
		}

		private void OnWindowStateEvent(object obj, WindowStateEventArgs args)
		{
			if ((args.Event.NewWindowState & Gdk.WindowState.Withdrawn) == 0)
			{
				settings.WindowMaximized =
					(args.Event.NewWindowState & Gdk.WindowState.Maximized) != 0;
			} 
		}

		private void OnKeyPressEvent(object obj, KeyPressEventArgs args)
		{
			if (args.Event.Key == Gdk.Key.Escape && searchPage != null &&
				mainWindow.Current == searchPage)
			{
				searchPage.StopSearch();
			}
		}

		#endregion

		private void SaveSettings()
		{
			settings.PageStyle = mainWindow.PageStyle;
			settings.ToolbarVisible = mainWindow.ToolbarVisible;
			if (searchPage != null)
			{
				settings.SearchColumns = searchPage.VisibleColumns;
			}
			if (downloadPage != null)
			{
				settings.DownloadColumns = downloadPage.VisibleColumns;
				settings.DownloadPageDirectoryTreeVisible = downloadPage.DirectoryTreeVisible;
				settings.DownloadPageSourcesVisible = downloadPage.SourcesVisible;
				settings.DownloadPagePanePosition = downloadPage.PanePosition;
			}
			if (uploadPage != null)
			{
				settings.UploadColumns = uploadPage.VisibleColumns;
			}
		}

		private static Widget CustomWidgetHandler(Glade.XML xml,
			string funcName, string name, string string1, string string2,
			int int1, int int2)
		{
			switch (funcName)
			{
				case "ChatView":
					return new ChatView();

				case "EncodingComboBox":
					return new EncodingComboBox(string1 == "withDefault");

				case "HistoryEntry":
					return new HistoryEntry();

				case "SearchEntry":
					return new SearchEntry();

				case "SearchFileTypeComboBox":
					return new SearchFileTypeComboBox();

				case "SearchResultView":
					return new SearchResultView();

				case "UserView":
					return new UserView(null);

				default:
					return null;
			}
		}

		private static void OpenUrl(Gtk.AboutDialog about, string url)
		{
			Util.OpenUrl(url);
		}

		#region Runtime event handlers

		private void OnHubConnected(object obj, ConnectionEventArgs args)
		{
			HubConnection hub = (HubConnection)args.Connection;
			InvokeAndWait(delegate
			{
				ShowHub(hub, false);

				// Enable searching
				if (searchPage != null)
				{
					searchPage.SetSearchEnabled(true);
				}
			});

			hub.PrivateMessage += OnPrivateMessage;
		}

		private void OnHubDisconnected(object obj, ConnectionEventArgs args)
		{
			HubConnection hub = (HubConnection)args.Connection;

			hub.PrivateMessage -= OnPrivateMessage;
			hub.Redirected -= OnHubRedirected;

			Application.Invoke(delegate
			{
				// Disable searching
				if (searchPage != null && runtime.HubManager.Count == 0)
				{
					searchPage.SetSearchEnabled(false);
				}
			});
		}

		private void OnHubRedirected(object sender, RedirectedEventArgs args)
		{
			HubConnection hub = (HubConnection)sender;
			if (runtime.HubManager.GetHub(args.Target) != null)
			{
				return;
			}

			Application.Invoke(delegate
			{
				string primary = String.Format(
					Catalog.GetString("Connect to \"{0}\"?"), args.Target);
				string secondary = String.Format(
					Catalog.GetString("You are being redirected from the hub \"{0}\" to another hub. Do you want to connect to it?"),
					Util.GetHubName(hub));

				MessageDialog dialog = new MessageDialog(primary, secondary, null,
					MessageType.Question, MainWindow);
				dialog.AddButton(Stock.Cancel, ResponseType.Cancel);
				dialog.AddButton(Stock.Connect, ResponseType.Accept);
				dialog.DefaultResponse = ResponseType.Accept;

				ResponseType response = (ResponseType)dialog.Run();
				if (response == ResponseType.Accept)
				{
					runtime.HubManager.ConnectTo(args.Target, hub.LocalIdentity,
						null, null);
				}
				dialog.Destroy();
			});
		}

		private void OnPrivateMessage(object obj, MessageEventArgs args)
		{
			Application.Invoke(delegate
			{
				HubConnection hub = (HubConnection)obj;
				Identity remoteIdentity = args.From == hub.LocalIdentity ?
					args.To : args.From;

				ChatPage chatPage = ShowChat(hub.LocalIdentity.User,
					remoteIdentity.User, false);

				chatPage.DisplayMessage(args.From.User, args.Message);
			});
		}

		private void OnUploadStarted(object obj, UploadEventArgs args)
		{
			if (args.Uploader.Request is FileRequest)
			{
				InvokeAndWait(delegate
				{
					ShowUploads(false);
				});
			}
		}

		private void OnDownloaderAdded(object obj, DownloadEventArgs args)
		{
			if (args.Downloader is FileDownloader)
			{
				Application.Invoke(delegate
				{
					ShowDownloads(false);
				});
			}
		}

		private void OnHubConnectionCreated(object obj, ConnectionEventArgs args)
		{
			HubConnection hub = args.Connection as HubConnection;
			if (hub != null)
			{
				InvokeAndWait(delegate
				{
					ShowHub(hub, false);
				});
				hub.Redirected += OnHubRedirected;
			}
		}

		private void OnRuntimeError(object obj, RuntimeErrorEventArgs args)
		{
			InvokeAndWait(delegate
			{
				OnRuntimeError(args.Error);
			});
		}

		private void OnRuntimeError(RuntimeError error)
		{
			string primary;
			string secondary = null;
			Window parent = mainWindow;

			switch (error)
			{
				case RuntimeError.ServerStart:
					primary = Catalog.GetString("Could not listen to port {0}");
					primary = String.Format(primary, runtime.ConnectionSettings.Port);

					secondary = Catalog.GetString("Please select another port in the preferences window.");

					EditPreferences();
					parent = preferencesWindow.Window;
					break;
				default:
					primary = Catalog.GetString("An error occured");
					break;
			}

			MessageDialog dialog = new MessageDialog(primary, secondary,
				null, MessageType.Error, parent);
			dialog.AddButton(Stock.Ok, ResponseType.Ok);

			dialog.Run();
			dialog.Destroy();
		}

		private static void OnUnhandledException(object obj,
			UnhandledExceptionEventArgs args)
		{
			Exception e = (Exception)args.ExceptionObject;

			// Exception details
			StringBuilder details = new StringBuilder();
			details.AppendLine(Catalog.GetString("An unhandled exception occured:"));
			details.AppendLine();
			details.AppendLine(e.ToString());
			details.AppendLine();

			details.AppendLine(String.Format("Version: {0}", Util.AppVersion));
			details.AppendLine(String.Format("Runtime Version: {0}", Environment.Version.ToString()));

			details.AppendLine("Assemblies:");
			foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies ())
			{
				AssemblyName name = a.GetName();
				details.AppendLine(String.Format("\t{0} ({1})", name.Name,
					name.Version.ToString()));
			}

			// Info
			string primary = Catalog.GetString("An unhandled exception occured");
			string secondary = Catalog.GetString("Please report this to the developers. Visit {0} for more information.");
			secondary = String.Format(secondary, Util.Website);

			Console.Error.WriteLine(primary);
			Console.Error.WriteLine(secondary);
			Console.Error.WriteLine(e);

			InvokeAndWait(delegate
			{
				// Create dialog
				Window parent = Globals.Gui.MainWindow;

				MessageDialog dialog = new MessageDialog(primary, secondary,
					details.ToString(), MessageType.Error, parent);
				dialog.AddButton(Stock.Ok, ResponseType.Ok);

				if (parent != null && parent.Visible)
				{
					dialog.SkipPagerHint = true;
					dialog.SkipTaskbarHint = true;
				}

				dialog.Run();
				dialog.Destroy();

				Application.Quit();
			});
		}

		#endregion

		#endregion
	}
}
